001    /*
002     * Copyright 2006 Stephen J. McConnell.
003     *
004     * Licensed  under the  Apache License,  Version 2.0  (the "License");
005     * you may not use  this file  except in  compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed  under the  License is distributed on an "AS IS" BASIS,
012     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
013     * implied.
014     *
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package net.dpml.lang;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.net.URI;
024    import java.net.URL;
025    import java.util.Map;
026    import java.util.Hashtable;
027    import java.lang.ref.WeakReference;
028    
029    import net.dpml.transit.link.ArtifactLinkManager;
030    
031    import net.dpml.util.Logger;
032    import net.dpml.util.DefaultLogger;
033    import net.dpml.util.ElementHelper;
034    import net.dpml.util.DOM3DocumentBuilder;
035    import net.dpml.util.Decoder;
036    import net.dpml.util.DecoderFactory;
037    import net.dpml.util.DecodingException;
038    import net.dpml.util.Resolver;
039    import net.dpml.util.SimpleResolver;
040    
041    import org.w3c.dom.Document;
042    import org.w3c.dom.Element;
043    import org.w3c.dom.TypeInfo;
044    
045    /**
046     * Construct a part.
047     */
048    public final class PartDecoder implements Decoder
049    {
050       /**
051        * Part XSD uri.
052        */
053        public static final String PART_XSD_URI = "link:xsd:dpml/lang/dpml-part#1.0";
054    
055        private static final DOM3DocumentBuilder DOCUMENT_BUILDER = 
056          new DOM3DocumentBuilder();
057        
058        private static final ValueDecoder VALUE_DECODER = new ValueDecoder();
059        
060        private static final PartDecoder DECODER = new PartDecoder();
061        
062        private static final String BASEPATH = setupBasePathSpec();
063        
064       /**
065        * Get the singleton instance of the part decoder.
066        * @return the decoder instance.
067        */
068        public static PartDecoder getInstance()
069        {
070            return DECODER;
071        }
072        
073        private Map m_map = new Hashtable();
074        private Map m_builders = new Hashtable();
075        
076        private Logger m_logger;
077        
078        private PartDecoder()
079        {
080            m_logger = new DefaultLogger( "dpml.lang" );
081        }
082        
083       /**
084        * Load a part from a uri.
085        * @param uri the part uri
086        * @param cache if true parts are cached relative to the requested uri
087        * @return the part definition
088        * @exception IOException if an IO error occurs
089        */
090        public Part loadPart( URI uri, boolean cache ) throws IOException
091        {
092            if( null == uri )
093            {
094                throw new NullPointerException( "uri" );
095            }
096            if( getLogger().isDebugEnabled() )
097            {
098                String path = getPartSpec( uri );
099                if( getLogger().isTraceEnabled() )
100                {
101                    if( cache )
102                    {
103                        getLogger().trace( "loading part (cache enabled): " + path );
104                    }
105                    else
106                    {
107                        getLogger().trace( "loading part (cache disabled): " + path );
108                    }
109                }
110                else
111                {
112                    getLogger().debug( "loading part: " + path );
113                }
114            }
115            String key = buildKey( uri );
116            if( cache )
117            {
118                WeakReference ref = (WeakReference) m_map.get( key );
119                if( null != ref )
120                {
121                    Part part = (Part) ref.get();
122                    if( null != part )
123                    {
124                        if( getLogger().isDebugEnabled() )
125                        {
126                            getLogger().debug( "located part in cache" );
127                        }
128                        return part;
129                    }
130                }
131            }
132            
133            // cache based retrieval was disabled or no cache value present
134            
135            try
136            {
137                final Document document = DOCUMENT_BUILDER.parse( uri );
138                final Element root = document.getDocumentElement();
139                Resolver resolver = new SimpleResolver();
140                Part value = decodePart( uri, root, resolver );
141                if( cache )
142                {
143                    WeakReference reference = new WeakReference( value );
144                    m_map.put( key, reference );
145                    if( getLogger().isTraceEnabled() )
146                    {
147                        getLogger().trace( "caching part" 
148                          + "\n  uri: " + uri
149                          + "\n  key: " + key ); 
150                    }
151                }
152                return value;
153            }
154            catch( Throwable e )
155            {
156                final String error =
157                  "An error while attempting to load a part."
158                  + "\n  uri: " + uri;
159                IOException exception = new IOException( error );
160                exception.initCause( e );
161                throw exception;
162            }
163        }
164        
165        private String buildKey( URI uri ) throws IOException
166        {
167            ClassLoader classloader = getAnchorClassLoader();
168            int n = System.identityHashCode( classloader );
169            return "" + n + "#" + getRealURI( uri ).toASCIIString();
170        }
171        
172        private String getID()
173        {
174            ClassLoader classloader = getAnchorClassLoader();
175            int n = System.identityHashCode( classloader );
176            return "" + n;
177        }
178        
179        private URI getRealURI( URI uri ) throws IOException
180        {
181            if( "link".equals( uri.getScheme() ) )
182            {
183                ArtifactLinkManager manager = new ArtifactLinkManager();
184                return manager.getTargetURI( uri );
185            }
186            else
187            {
188                return uri;
189            }
190        }
191        
192       /**
193        * Resolve a object from a DOM element.
194        * @param element the dom element
195        * @param resolver build-time value resolver
196        * @return the resolved object
197        * @exception IOException if an error occurs during element evaluation
198        */
199        public Object decode( Element element, Resolver resolver ) throws IOException
200        {
201            return decodePart( null, element, resolver );
202        }
203        
204       /**
205        * Resolve a part from a DOM element.
206        * @param uri the part uri
207        * @param element element part definition
208        * @param resolver build-time value resolver
209        * @return the resolved part datastructure
210        * @exception IOException if an error occurs during element evaluation
211        */
212        public Part decodePart( URI uri, Element element, Resolver resolver ) throws IOException
213        {
214            TypeInfo info = element.getSchemaTypeInfo();
215            String namespace = info.getTypeNamespace();
216            if( PART_XSD_URI.equals( namespace ) )
217            {
218                boolean alias = ElementHelper.getBooleanAttribute( element, "alias", false );
219                Info information = getInfo( uri, element );
220                Classpath classpath = getClasspath( element );
221                Element strategy = getStrategyElement( element );
222                return build( m_logger, information, classpath, strategy, resolver );
223            }
224            else
225            {
226                final String error = 
227                  "Part namespace not recognized."
228                  + "\nExpecting: " + PART_XSD_URI
229                  + "\nFound: " + namespace;
230                throw new DecodingException( element, error );
231            }
232        }
233        
234       /**
235        * Resolve a part plugin or resource strategy.
236        * @param logger the logging channel
237        * @param information the part info definition
238        * @param classpath the part classpath definition
239        * @param strategy part deployment strategy definition
240        * @param resolver build-time value resolver
241        * @return the resolved part
242        * @exception IOException if an error occurs during element evaluation
243        */
244        public Part build( 
245          Logger logger, Info information, Classpath classpath, Element strategy, Resolver resolver ) 
246          throws IOException
247        {
248            ClassLoader anchor = getAnchorClassLoader();
249            TypeInfo info = strategy.getSchemaTypeInfo();
250            String namespace = info.getTypeNamespace();
251            if( PART_XSD_URI.equals( namespace ) )
252            {
253                // this is either a plugin or a resource
254                
255                String name = info.getTypeName();
256                if( "plugin".equals( name ) )
257                {
258                    if( logger.isTraceEnabled() )
259                    {
260                        logger.trace( "reading plugin definition" );
261                    }
262                    String classname = ElementHelper.getAttribute( strategy, "class" );
263                    Element[] elements = ElementHelper.getChildren( strategy );
264                    Value[] values = VALUE_DECODER.decodeValues( elements );
265                    Part part = new Plugin( logger, information, classpath, classname, values );
266                    if( logger.isTraceEnabled() )
267                    {
268                        logger.trace( "loaded plugin definition" );
269                    }
270                    return part;
271                }
272                else if( "resource".equals( name ) )
273                {
274                    if( logger.isTraceEnabled() )
275                    {
276                        logger.trace( "reading resource definition" );
277                    }
278                    String urn = ElementHelper.getAttribute( strategy, "urn" );
279                    String path = ElementHelper.getAttribute( strategy, "path" );
280                    Part part = new Resource( logger, information, classpath, urn, path );
281                    if( logger.isTraceEnabled() )
282                    {
283                        logger.trace( "loaded resource definition" );
284                    }
285                    return part;
286                }
287                else
288                {
289                    final String error = 
290                      "Element type name ["
291                      + name
292                      + "] is not a recognized element type within the "
293                      + PART_XSD_URI
294                      + " namespace.";
295                    throw new DecodingException( strategy, error );
296                }
297            }
298            else
299            {
300                // this is a foreign part
301                
302                try
303                {
304                    URI uri = getDecoderURI( strategy );
305                    Builder builder = loadForeignBuilder( uri );
306                    if( logger.isTraceEnabled() )
307                    {
308                        logger.trace( 
309                          "using builder [" 
310                          + builder.getClass().getName() 
311                          + "]" );
312                    }
313                    Part part = builder.build( logger, information, classpath, strategy, resolver );
314                    if( logger.isTraceEnabled() )
315                    {
316                        logger.trace( 
317                          "loaded part ["  
318                          + part.getClass().getName() 
319                          + "]" );
320                    }
321                    return part;
322                }
323                catch( Exception ioe )
324                {
325                    final String error = 
326                      "Internal error while attempting to load foreign part.";
327                    throw new DecodingException( strategy, error, ioe );
328                }
329                finally
330                {
331                    Thread.currentThread().setContextClassLoader( anchor );
332                }
333            }
334        }
335        
336       /**
337        * Resolve the element decoder uri.
338        *
339        * @param element the DOM element
340        * @return the decoder uri
341        * @exception DecodingException if an error occurs
342        */
343        public URI getDecoderURI( Element element ) throws DecodingException
344        {
345            String uri = ElementHelper.getAttribute( element, "handler" );
346            if( null != uri )
347            {
348                try
349                {
350                    return new URI( uri );
351                }
352                catch( Exception e )
353                {
354                    final String error = 
355                      "Internal error while resolving handler attribute (expecting uri value)";
356                    throw new DecodingException( element, error, e );
357                }
358            }
359            TypeInfo info = element.getSchemaTypeInfo();
360            String namespace = info.getTypeNamespace();
361            try
362            {
363                return DecoderFactory.getDecoderURIFromNamespaceURI( namespace );
364            }
365            catch( Exception e )
366            {
367                final String error = 
368                  "Internal error while attempting to resolve default decoder uri.";
369                throw new DecodingException( element, error, e );
370            }
371        }
372        
373       /**
374        * Get the assigned logging channel.
375        * @return the logging channel
376        */
377        protected Logger getLogger()
378        {
379            return m_logger;
380        }
381        
382       /**
383        * Load a forign part builder.  The implementation will attempt to resolve a 
384        * plugin defintion from the supplied uri, caching a reference to
385        * the builder, and returning the plugin instance as a builder instance.
386        *
387        * @param uri the part builder uri
388        * @see Builder
389        * @exception DecodingException if a part decoding error occurs
390        * @exception Exception if part loading error occurs
391        */
392        private Builder loadForeignBuilder( URI uri ) throws DecodingException, Exception
393        {
394            WeakReference ref = (WeakReference) m_builders.get( uri );
395            if( null != ref )
396            {
397                Builder builder = (Builder) ref.get();
398                if( null != builder )
399                {
400                    if( getLogger().isTraceEnabled() )
401                    {
402                        getLogger().trace( "located builder [" + uri + "]" );
403                    }
404                    return builder;
405                }
406                else
407                {
408                    if( getLogger().isTraceEnabled() )
409                    {
410                        getLogger().trace( "reloading builder [" + uri + "]" );
411                    }
412                }
413            }
414            else
415            {
416                if( getLogger().isTraceEnabled() )
417                {
418                    getLogger().trace( "loading builder [" + uri + "]" );
419                }
420            }
421            
422            Part part = loadPart( uri, true );
423            Logger logger = getLogger();
424            Object[] args = new Object[]{logger};
425            Object object = part.instantiate( args );
426            if( object instanceof Builder )
427            {
428                Builder builder = (Builder) object;
429                WeakReference reference = new WeakReference( builder );
430                m_builders.put( uri, reference );
431                return builder;
432            }
433            else
434            {
435                final String error = 
436                  "Plugin does not implement the "
437                  + Builder.class.getName()
438                  + " interface."
439                  + "\nURI: " + uri 
440                  + "\nClass: " + object.getClass().getName();
441                throw new PartException( error );
442            }
443        }
444        
445        private Element getStrategyElement( Element root ) throws DecodingException
446        {
447            Element[] children = ElementHelper.getChildren( root );
448            if( children.length != 3 )
449            {
450                final String error = 
451                  "Illegal number of child elements in <part>. Expecting 3, found " 
452                  + children.length
453                  + ".";
454                throw new DecodingException( root, error );
455            }
456            return children[1];
457        }
458        
459        
460        private Info getInfo( URI uri, Element root )
461        {
462            Element element = ElementHelper.getChild( root, "info" );
463            String title = ElementHelper.getAttribute( element, "title" );
464            Element descriptionElement = ElementHelper.getChild( element, "description" );
465            String description = ElementHelper.getValue( descriptionElement );
466            return new Info( uri, title, description );
467        }
468        
469       /**
470        * Construct the classpath defintion.
471        * @param root the element containing a 'classpath' element.
472        * @return the classpath definition
473        * @exception DecodingException if an error occurs during element evaluation
474        */
475        protected Classpath getClasspath( Element root ) throws DecodingException
476        {
477            Element classpath = ElementHelper.getChild( root, "classpath" );
478            if( null == classpath )
479            {
480                final String error = 
481                  "Required classpath element is not present in plugin descriptor.";
482                throw new DecodingException( root, error );
483            }
484            
485            try
486            {
487                Element[] children = ElementHelper.getChildren( classpath );
488                URI[] sys = buildURIs( classpath, "system" );
489                URI[] pub = buildURIs( classpath, "public" );
490                URI[] prot = buildURIs( classpath, "protected" );
491                URI[] priv = buildURIs( classpath, "private" );
492                Classpath cp = new Classpath( sys, pub, prot, priv );
493                return cp;
494            }
495            catch( Throwable e )
496            {
497                final String error = 
498                  "Unable to decode classpath due to an unexpected error.";
499                throw new DecodingException( classpath, error, e );
500            }
501        }
502        
503        private URI[] buildURIs( Element classpath, String key ) throws Exception
504        {
505            Element category = ElementHelper.getChild( classpath, key );
506            if( null == category )
507            {
508                return new URI[0];
509            }
510            else
511            {
512                Element[] children = ElementHelper.getChildren( category, "uri" );
513                URI[] uris = new URI[ children.length ];
514                for( int i=0; i<children.length; i++ )
515                {
516                    Element child = children[i];
517                    String value = ElementHelper.getValue( child );
518                    uris[i] = new URI( value );
519                }
520                return uris;
521            }
522        }
523        
524        private ClassLoader getAnchorClassLoader()
525        {
526            ClassLoader classloader = Thread.currentThread().getContextClassLoader();
527            if( null == classloader )
528            {
529                return Part.class.getClassLoader();
530            }
531            else
532            {
533                return classloader;
534            }
535        }
536        
537        private static String setupBasePathSpec()
538        {
539            try
540            {
541                String path = System.getProperty( "user.dir" );
542                File file = new File( path );
543                URI uri = file.toURI();
544                URL url = file.toURL();
545                return url.toString();
546            }
547            catch( Exception e )
548            {   
549                return e.toString();
550            }
551        }
552        
553        static String getPartSpec( URI uri )
554        {
555            String path = uri.toASCIIString();
556            if( path.startsWith( BASEPATH ) )
557            {
558                return "./" + path.substring( BASEPATH.length() );
559            }
560            else
561            {
562                return path;
563            }
564        }
565    }